//! Data driven pages

mod memory;
mod md;

use std::{collections::{HashMap, HashSet}, future::Future, path::PathBuf};

use axum::{body::{Body, Bytes}, extract::{Path, Query, State}, http::StatusCode, routing::get, Extension, Router};
use serde_derive::Deserialize;
use tera::{Context, Map, Value};
use tracing::debug;

use crate::{app::raw_error, EdgeError, Result};

use super::{redir_to_folder, service_tree::ServiceParts, AppState, HandlerRV, UserSessionState};

static INDEX_TEMPLATE_NAME: &str = "index.html";
static ITEM_TEMPLATE_NAME: &str = "item.html";


/// Item range
#[derive(Debug, Clone, Copy)]
pub struct Range {
    pub start: usize,
    pub len: usize,
}

impl Range {
    pub const MAX: Self = Self { start: 0, len: usize::MAX };
    pub fn project<T>(self, r: &[T]) -> &[T] { &r[self.start().min(r.len())..self.end().min(r.len())] }
    #[inline]
    pub fn end(&self) -> usize { self.start + self.len }
    //#[inline]
    //pub fn len(&self) -> usize { self.len }
    #[inline]
    pub fn start(&self) -> usize { self.start }
}

/// Our boxed future
pub type Fx<T> = Box<dyn Future<Output=T> + Send + Unpin + 'static>;

pub fn fx_ready<T: Send + 'static>(w: T) -> Fx<T> {
    Box::new(std::future::ready(w))
}

#[allow(unused)]
pub enum Resource {
    File(std::path::PathBuf),
    Bytes(Bytes),
    Body(Body),
}


/// Generic item collection API
/// The underlying storage may be in-memory, disk or RDBMS
pub trait CollectionProvider {
    fn get_item(&self, key: &str) -> Fx<Option<Value>>;
    fn get_index_range(&self, range: Range) -> Fx<Vec<Value>>;
    fn get_item_resource(&self, key: &str, path: &str) -> Fx<Option<Resource>>;
}

pub struct Collection {
    provider: Box<dyn CollectionProvider + Send + Sync + 'static>,
    /// Field name for the key, typically "key"
    key_field_name: String,
    /// Field name for the item reference, typically "item_ref"
    item_ref_field_name: String,
} 

pub struct DataDrivenPageService {
    collections: HashMap<String, Collection>,
}


fn clone_with(context: &Context, key: &str, val: &Value) -> Context {
    let mut context = context.clone();
    context.insert(key, val);
    context
}

impl DataDrivenPageService {

    pub fn new(collections: HashMap<String, Collection>) -> Self {
        Self { collections }
    }

    pub fn has_service(&self, name: &str) -> bool { self.collections.contains_key(name) }

    pub fn create_router<'t>(&self, state: AppState, service_parts: ServiceParts<'t>) -> Router<AppState> {
        let coll_name = service_parts.root_as_str();

        assert!(self.collections.contains_key(coll_name), "attempt to create router for non-existent coll");
        let templates: HashSet<&str> = service_parts.templates.into_iter().collect();
        assert_eq!(templates, [INDEX_TEMPLATE_NAME, ITEM_TEMPLATE_NAME].into_iter().collect(), "wrong collection template set");
 
        let r = Router::new()
            .route("/", get(get_ddpages_index))
            .route("/:key", get(redir_to_folder))
            .route("/:key/", get(get_ddpages_item))
            .route("/:key/*resource", get(get_ddpages_item_resource));
    
        r.with_state(state.with_root(coll_name.to_owned()))
    }

    pub async fn get_index_context<'s>(&'s self, coll_name: &'s str, range: Option<Range>, context: &Context) -> Option<Context> {
        let Some(coll) = self.collections.get(coll_name) else { return None };
        let mut index = coll.provider.get_index_range(range.unwrap_or(Range::MAX)).await;
        // into each index entry, insert the item reference
        for w in &mut index {
            if let Some(key) = w.get(&coll.key_field_name).and_then(stringize_key_value) {
                w[&coll.item_ref_field_name] = Value::String(format!("/{coll_name}/{key}/"));
            }
        }
        return Some(clone_with(context, "i", &Value::Array(index)));
    }

    pub async fn get_item_context<'s, 'x>(&'s self, coll_name: &'s str, key: &'x str, context: &Context) -> Option<Context> {
        let Some(coll) = self.collections.get(coll_name) else { return None };
        coll.provider.get_item(key).await.map(|w| clone_with(context, "d", &w))
    }

    pub async fn get_item_resource<'s, 'x>(&'s self, coll_name: &'s str, key: &'x str, resource: &'x str) -> Result<Body> {
        let Some(coll) = self.collections.get(coll_name) else { return Err(EdgeError::NoCollection(coll_name.to_owned())) };
        match coll.provider.get_item_resource(key, resource).await {
            Some(Resource::File(path)) => Ok(Body::from(std::fs::read(path)?)),
            Some(Resource::Bytes(bytes)) => Ok(Body::from(bytes)),
            Some(Resource::Body(body)) => Ok(body),
            None => Err(EdgeError::ResourceNotFound(coll_name.to_owned(), key.to_owned(), resource.to_owned()))
        }
    }

}

/// Returns a string as-is, a number converted to string; other types yield None
pub fn stringize_key_value(w: &Value) -> Option<String> {
    match w {
        Value::String(s) => Some(s.to_string()),
        Value::Number(n) => Some(n.to_string()),
        _ => None
    }
}

async fn get_ddpages_index(
    State(AppState { root: coll_name, render, ..}): State<AppState>,
    Extension(uss): Extension<UserSessionState>,
    Query(query): Query<QueryArgs>,
) -> HandlerRV {
    debug!(message="get_ddpages_index", coll_name=coll_name.as_ref());

    let Some(context) = render.dd_pages.get_index_context(
        &coll_name,
        query.range_opt(), 
        &uss.context,
    ).await else {
        return render.e404.clone()
    };

    let template_name = format!("{coll_name}/{INDEX_TEMPLATE_NAME}");

    //debug!(template_name=template_name);
    render.render(&template_name, &context)
}

async fn get_ddpages_item(
    State(AppState { root: coll_name, render, ..}): State<AppState>,
    Extension(uss): Extension<UserSessionState>,
    Path(key): Path<String>,
) -> HandlerRV {
    debug!(message="get_ddpages_item", coll_name=coll_name.as_ref(), key=key);

    let Some(context) = render.dd_pages.get_item_context(&coll_name, &key, &uss.context).await else {
        return render.e404.clone()
    };

    let template_name = format!("{coll_name}/{ITEM_TEMPLATE_NAME}");

    //debug!(template_name=template_name);
    render.render(&template_name, &context)
}

async fn get_ddpages_item_resource(
    State(AppState { root: coll_name, render, ..}): State<AppState>,
    Path((key, resource)): Path<(String, String)>,
) -> (StatusCode, Body) {
    debug!(message="get_ddpages_item_resource", coll_name=coll_name.as_ref(), key=key, resource=resource);
    match render.dd_pages.get_item_resource(&coll_name, &key, &resource).await {
        Ok(body) => (StatusCode::OK, body),
        Err(EdgeError::ResourceNotFound(_, _, _)) => (StatusCode::NOT_FOUND, Body::from(raw_error("not found"))),
        Err(e) => (StatusCode::SERVICE_UNAVAILABLE, Body::from(raw_error(e))),
    }
}

/*async fn get_ddpages(
    State((root, state)): State<ServiceState>, 
    matched_path: MatchedPath,
    Query(query): Query<QueryArgs>,
) -> HandlerRV {
    debug!(message="get_ddpages", root=root.as_ref(), matched_path=matched_path.as_str());

    let result = 
        state.dd_pages.get(matched_path.as_str(), query.range_opt(), state.context.clone()).await;

    let Some((template_name, context)) = result else { return state.e404.clone() };

    //let path = if matched_path.as_str() == "/" { "/index.html" } else { matched_path.as_str() };
    //let template_name = format!("{root}{path}");
    //let context = &state.context;

    //debug!(template_name=template_name);
    state.render(&template_name, &context)
}*/

#[derive(Deserialize, Default)]
struct QueryArgs {
    off: Option<usize>,
    len: Option<usize>,
}

impl QueryArgs {
    //pub const DEFAULT_PAGE_LEN
    fn range_opt(&self) -> Option<Range> {
        match self {
            Self { off: None, len: None} => None,
            Self { off, len } => 
                Some(Range { start: off.unwrap_or(0), len: len.unwrap_or(Range::MAX.len) }),
        }
    }
}




#[cfg(test)]
fn ___check_futures(ds: &DataDrivenPageService, cx: &Context) {
    fn is_send<T: Send>(_: T) { }
    is_send(ds.get_index_context("key", None, cx));
}





fn default_key_field_name() -> String { "key".to_string() }
fn default_item_ref_field_name() -> String { "item_ref".to_string() }

#[derive(Deserialize, Debug)]
pub struct CollectionConfig {
    /// routing key where this collection is mounted, with no trailing / or .html. E.g. "smth/docs" or just "docs"
    pub key: String,
    /// Typically "key"
    #[serde(default="default_key_field_name")]
    pub key_field_name: String,
    /// Typically "item_ref"
    #[serde(default="default_item_ref_field_name")]
    pub item_ref_field_name: String,
    /// data fields to be selected for index
    pub index_fields: Vec<String>,
    /// type of provider, e.g. "memory"
    pub provider_type: String,
    /// provider-specific configuration
    pub provider_config: toml::Value,
}


#[derive(Deserialize, Debug)]
pub struct DataDrivenPagesConfig {
    pub collections: Vec<CollectionConfig>,
}

pub fn create_data_driven_page_service(config: DataDrivenPagesConfig, data_dir: PathBuf) -> Result<DataDrivenPageService> {
    let mut collections = HashMap::new();
    for c in config.collections {
        if c.key.strip_suffix(".html").is_some() {
            panic!("collection key must not contain '.html' suffix (got '{}')", c.key);
        }
        if c.key.strip_suffix("/").is_some() {
            panic!("collection key must not end with / (got '{}')", c.key);
        }
        if c.index_fields[0] != c.key_field_name {
            panic!("key field must be 1st field in index  (got '{}' and '{}')", c.key_field_name, c.index_fields[0]);
        }
        //let index_t = c.index_template.unwrap_or_else(|| c.key.clone() + "/_index.html");
        //let item_t = c.item_template.unwrap_or_else(|| c.key.clone() + "/_item.html");
        //let index_t = c.index_template.unwrap_or_else(|| c.key.clone() + ".html");
        //let item_t = c.item_template.unwrap_or_else(|| c.key.clone() + "_item_.html");

        let provider_data_dir = data_dir.join(&c.key);

        let provider = match c.provider_type.as_ref() {
            "memory" => Box::new(memory::create_provider(c.index_fields, c.provider_config, provider_data_dir)?),
            other => panic!("invalid data source provider type (got '{other}')"),
        };

        let key_field_name = c.key_field_name;
        let item_ref_field_name = c.item_ref_field_name;
        collections.insert(c.key, Collection { provider, key_field_name, item_ref_field_name });
    }
    Ok(DataDrivenPageService::new(collections))
}